package autotest;

import java.util.ArrayList;
import java.util.Collection;

import javax.swing.JOptionPane;

import junit.framework.TestResult;
import junit.framework.TestSuite;

/**
 * Classe de démarrage des tests. Point d'entrée de la partie testeur de
 * l'application.
 * 
 * @author gnpt
 */
public class AutoTest {

	/* L'instance courante (Singleton Pattern) */
	private static AutoTest instance = null;
	/* Pour savoir si on s'est self-testé avant de s'être utilisé */
	private boolean selfTest;
	private TestResult results;
	private boolean printResults;

	/**
	 * Constructeur par défaut privé.
	 */
	private AutoTest() {
		this.selfTest = false;
		this.printResults = false;
		this.results = null;
	}

	/**
	 * Accesseur pour l'instance statique d'AutoTest. Elle est crée si elle
	 * n'existait pas.
	 * 
	 * @return l'instance statique d'AutoTest.
	 */
	public static AutoTest instance() {
		if (AutoTest.instance == null) {
			AutoTest.instance = new AutoTest();
		}
		return AutoTest.instance;
	}

	/**
	 * Accesseur pour le booléen qui demande l'affichage du résultat (sommaire)
	 * des tests sur la sortie standard.
	 * 
	 * @return {@code true} si activé, {@code false} sinon.
	 */
	public boolean isPrintResults() {
		return printResults;
	}

	/**
	 * Mutateur pour activer ou non l'affichage du résultat des tests sur la
	 * sortie standard.
	 * 
	 * @param printResults
	 *            {@code true} pour l'activer, {@code false} sinon.
	 */
	public void setPrintResults(boolean printResults) {
		this.printResults = printResults;
	}

	/**
	 * Accesseur pour les résultats de l'autotest de cette classe.
	 * 
	 * @return l'objet {@link junit.framework.TestResult} contenant les
	 *         résultats.
	 */
	public TestResult getResults() {
		return results;
	}

	/**
	 * Teste les classes passées en paramètres, et retourne les résultats. Si un
	 * nom de classe n'a pu être résolu, le TestResult associé n'existera pas
	 * dans la Collection résultat. Il est ensuite difficile de déterminer à
	 * quelle classe appartient tel TestResult.
	 * <p>
	 * Pour remédier à ce problème, il vaut mieux faire la résolution des noms
	 * de classe séparément, et utiliser la méthode
	 * {@link AutoTest#run(Class...)}.
	 * 
	 * @param nomsClasse
	 *            une liste de noms de classe à tester.
	 * @return la collection des résultats des tests.
	 */
	public static Collection<TestResult> run(String... nomsClasse) {
		/* On crée la collection résultat */
		Collection<TestResult> collRes;
		collRes = new ArrayList<TestResult>();

		TestResult res;

		for (String nomClasse : nomsClasse) {
			try {
				Class<?> klass = Class.forName(nomClasse);
				res = AutoTest.run(klass);
				collRes.add(res);
			} catch (ClassNotFoundException e) {
				AutoTest.instance.afficherAvertissement("La classe "
						+ nomClasse + " n'a pas pu être chargée.");
			} catch (NoClassDefFoundError e) {
				AutoTest.instance.afficherAvertissement("La classe "
						+ nomClasse + " n'a pas pu être chargée.");
			}
		}
		return collRes;
	}

	/**
	 * Teste les classes passées en paramètres.
	 * 
	 * @param classes
	 *            une liste de classes à tester.
	 * @return une collection de résultats des tests.
	 */
	public static Collection<TestResult> run(Class<?>... classes) {
		Collection<TestResult> collRes;
		collRes = new ArrayList<TestResult>();

		for (Class<?> classe : classes) {
			collRes.add(AutoTest.run(classe));
		}
		return collRes;
	}

	/**
	 * Méthode d'exécution des tests pour la classe donnée.
	 * 
	 * @param klass
	 *            la classe à tester.
	 * @return un TestResult contenant les résultats des tests.
	 */
	public static TestResult run(Class<?> klass) {
		/* On récupère l'instance de classe */
		AutoTest test = AutoTest.instance();
		if (test.selfTest == false) {
			/**
			 * On s'AutoTest si ça n'a pas déjà été fait. Du fait que cette
			 * méthode va s'appeler elle-même, on doit d'abord considéré le test
			 * comme ayant été réussi pour éviter de boucler indéfiniement.
			 */
			test.selfTest = true;
			test.results = AutoTest.run(test.getClass());

			if (!test.results.wasSuccessful()) {
				test.afficherErreur("Les tests de AutoTest ont échoué.\n");
			}
		}

		/* On prépare le nom des classes à aller chercher */
		String testName = test.getNomTypeClassesATester(klass);

		/* Une liste qui contiendra les cas de test */
		Collection<Class<?>> testClasses;
		testClasses = new ArrayList<Class<?>>(5);

		int i = 0;
		while (++i > 0) {
			try {
				/* Chargement de la classe */
				testClasses.add(Class.forName(testName + i));
			} catch (ClassNotFoundException e) {
				if (i == 1) {
					test
							.afficherInformation("Aucune classe de test n'a été trouvée pour\n la classe "
									+ klass.getCanonicalName()
									+ ".\n Les tests seront donc supposés sans échecs/erreurs.");

				} // Si i > 1, on a au moins trouvé une classe de test
				/* On sort de la boucle */
				break;
			}
		}

		/* On construit la TestSuite et son TestResult */
		TestSuite ts = new TestSuite(testClasses.toArray(new Class<?>[] {}));
		TestResult tr = new TestResult();
		/* Exécution des tests */
		ts.run(tr);

		if (test.printResults) {
			test.printTestResult(tr, klass);
		}
		return tr;
	}

	/**
	 * Point d'entrée de la classe, sans grande prétention. Ce code se contente
	 * de l'autoTester.
	 */
	public static void main(String args[]) {
		AutoTest self = AutoTest.instance();
		self.setPrintResults(true);
		self.selfTest = true;
		AutoTest.run(AutoTest.class);

	}

	/**
	 * Affiche le contenu d'un TestResult sur la flux {@code out}.
	 * 
	 * @param run
	 *            le TestResult à afficher.
	 * @param klass
	 *            la classe testée dont le résultat est décrit par run.
	 * @param out
	 *            le flux sur lequel on fait l'affichage.
	 */
	private void printTestResult(TestResult run, Class<?> klass,
			java.io.PrintStream out) {
		StringBuffer buf = new StringBuffer();
		/* En-tête de l'affichage */
		buf.append("Résultat des tests pour la classe ");
		buf.append(klass.getCanonicalName());
		if (run.wasSuccessful()) {
			buf.append(" : Réussi.");
		} else {
			buf.append(" : Echec.");
		}
		/* Détails */
		buf.append("\n\nTests tentés : " + run.runCount());
		buf.append("\nTests en échec : " + run.failureCount());
		buf.append("\nTests en erreur : " + run.errorCount());
		out.println(buf.append("\n").toString());
	}

	/**
	 * Affiche le contenu d'un TestResult sur la sortie standard.
	 * 
	 * @param run
	 *            le TestResult à afficher.
	 * @param klass
	 *            la classe testée dont le résultat est décrit par run.
	 */
	private void printTestResult(TestResult run, Class<?> klass) {
		printTestResult(run, klass, System.out);
	}

	/**
	 * Constructeur du nom qualifié des classes de test.
	 * <p>
	 * Par exemple, si la classe en argument correspond à {@code
	 * un.paquetage.UneClasse}, alors la fonction renverra la chaîne {@code
	 * un.paquetage.test.UneClasseTest}.
	 * 
	 * @param klass
	 *            la classe à partir de laquelle on construit le résultat.
	 * 
	 * @return une chaîne représentant le nom type des cas de test.
	 */
	public String getNomTypeClassesATester(Class<?> klass) {
		/* Nom de paquetage */
		Package paquetageCourant = klass.getPackage();
		String nomPaquetageCourant = paquetageCourant.getName();
		/* Nom des classes */
		String nomTypeClassesATester = klass.getSimpleName() + "Test";

		return nomPaquetageCourant + ".test." + nomTypeClassesATester;
	}

	/**
	 * Affiche une pop-up contenant un message d'erreur.
	 * 
	 * @param msg
	 *            le message à afficher.
	 */
	protected void afficherErreur(String msg) {
		JOptionPane.showMessageDialog(null, msg, "Erreur",
				JOptionPane.ERROR_MESSAGE);
	}

	/**
	 * Affiche une pop-up contenant un message d'avertissement.
	 * 
	 * @param msg
	 *            le message à afficher.
	 */
	protected void afficherAvertissement(String msg) {
		JOptionPane.showMessageDialog(null, msg, "Attention",
				JOptionPane.WARNING_MESSAGE);
	}

	/**
	 * Affiche une pop-up contenant un message informatif.
	 * 
	 * @param msg
	 *            le message à afficher.
	 */
	protected void afficherInformation(String msg) {
		JOptionPane.showMessageDialog(null, msg, "Information",
				JOptionPane.INFORMATION_MESSAGE);
	}

}
